Опубликовано: 17 авг. 2021 г., Последнее обновление: 25 сен. 2024 г.
Когда переход вида выполняется в одном документе, он называется переходом вида того же документа . Это обычно происходит в одностраничных приложениях (SPA), где JavaScript используется для обновления DOM. Переходы вида того же документа поддерживаются в Chrome, начиная с Chrome 111.
Чтобы запустить переход между представлениями одного документа, вызовите document.startViewTransition
:
function handleClick(e) {
// Fallback for browsers that don't support this API:
if (!document.startViewTransition) {
updateTheDOMSomehow();
return;
}
// With a View Transition:
document.startViewTransition(() => updateTheDOMSomehow());
}
При вызове браузер автоматически делает снимки всех элементов, для которых объявлено CSS-свойство view-transition-name
.
Затем он выполняет переданный обратный вызов, который обновляет DOM, после чего делает снимки нового состояния.
Эти снимки затем размещаются в дереве псевдоэлементов и анимируются с помощью мощи CSS-анимаций. Пары снимков из старого и нового состояния плавно переходят из своего старого положения и размера в новое местоположение, в то время как их содержимое плавно переходит. Если хотите, вы можете использовать CSS для настройки анимации.
Переход по умолчанию: кросс-фейд
Переход между представлениями по умолчанию представляет собой плавный переход, поэтому он служит хорошим введением в API:
function spaNavigate(data) {
// Fallback for browsers that don't support this API:
if (!document.startViewTransition) {
updateTheDOMSomehow(data);
return;
}
// With a transition:
document.startViewTransition(() => updateTheDOMSomehow(data));
}
Где updateTheDOMSomehow
изменяет DOM на новое состояние. Это можно сделать как угодно. Например, можно добавлять или удалять элементы, менять имена классов или менять стили.
И вот так страницы сменяют друг друга:
Ладно, кроссфейд не так уж и впечатляет. К счастью, переходы можно настраивать, но сначала нужно понять, как работает этот базовый кроссфейд.
Как работают эти переходы
Давайте обновим предыдущий пример кода.
document.startViewTransition(() => updateTheDOMSomehow(data));
При вызове .startViewTransition()
API фиксирует текущее состояние страницы. Это включает в себя создание снимка.
После завершения вызывается обратный вызов, переданный в .startViewTransition()
. Именно там изменяется DOM. Затем API фиксирует новое состояние страницы.
После захвата нового состояния API создает дерево псевдоэлементов, подобное следующему:
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
::view-transition
находится в наложении, поверх всего остального на странице. Это полезно, если вы хотите задать цвет фона для перехода.
::view-transition-old(root)
— скриншот старого представления, а ::view-transition-new(root)
— живое представление нового представления. Оба отображаются как CSS 'замененное содержимое' (как <img>
).
Старый вид анимируется от opacity: 1
до opacity: 0
, в то время как новый вид анимируется от opacity: 0
до opacity: 1
, создавая плавный переход.
Вся анимация выполняется с использованием CSS-анимаций, поэтому ее можно настраивать с помощью CSS.
Настройте переход
Все псевдоэлементы перехода вида могут быть нацелены с помощью CSS, и поскольку анимации определяются с помощью CSS, вы можете изменять их, используя существующие свойства анимации CSS. Например:
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 5s;
}
Благодаря этому изменению затухание теперь происходит очень медленно:
Ладно, это все еще не впечатляет. Вместо этого следующий код реализует общий переход оси Material Design :
@keyframes fade-in {
from { opacity: 0; }
}
@keyframes fade-out {
to { opacity: 0; }
}
@keyframes slide-from-right {
from { transform: translateX(30px); }
}
@keyframes slide-to-left {
to { transform: translateX(-30px); }
}
::view-transition-old(root) {
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}
::view-transition-new(root) {
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
И вот результат:
Переход нескольких элементов
В предыдущей демонстрации вся страница была вовлечена в переход общей оси. Это работает для большей части страницы, но это не совсем подходит для заголовка, так как он выезжает только для того, чтобы снова въехать.
Чтобы избежать этого, вы можете извлечь заголовок из остальной части страницы, чтобы его можно было анимировать отдельно. Это делается путем назначения view-transition-name
элементу.
.main-header {
view-transition-name: main-header;
}
Значение view-transition-name
может быть любым (за исключением none
, что означает, что имя перехода отсутствует). Оно используется для уникальной идентификации элемента в переходе.
И вот результат:
Теперь заголовок остается на месте и плавно переходит в режим плавного перехода.
Это объявление CSS привело к изменению дерева псевдоэлементов:
::view-transition
├─ ::view-transition-group(root)
│ └─ ::view-transition-image-pair(root)
│ ├─ ::view-transition-old(root)
│ └─ ::view-transition-new(root)
└─ ::view-transition-group(main-header)
└─ ::view-transition-image-pair(main-header)
├─ ::view-transition-old(main-header)
└─ ::view-transition-new(main-header)
Теперь есть две группы переходов. Одна для заголовка, а другая для всего остального. Они могут быть нацелены независимо с помощью CSS и заданы разными переходами. Хотя в этом случае main-header
был оставлен с переходом по умолчанию, который является кросс-фейдом.
Ну ладно, переход по умолчанию — это не просто плавный переход, ::view-transition-group
также выполняет переходы:
- Позиционирование и преобразование (с использованием
transform
) - Ширина
- Высота
До сих пор это не имело значения, так как заголовок имеет одинаковый размер и положение с обеих сторон DOM меняется. Но вы также можете извлечь текст из заголовка:
.main-header-text {
view-transition-name: main-header-text;
width: fit-content;
}
fit-content
используется, чтобы элемент был размером с текст, а не растягивался на оставшуюся ширину. Без этого обратная стрелка уменьшает размер элемента текста заголовка, а не делает его одинаковым на обеих страницах.
Итак, теперь у нас есть три части для игры:
::view-transition
├─ ::view-transition-group(root)
│ └─ …
├─ ::view-transition-group(main-header)
│ └─ …
└─ ::view-transition-group(main-header-text)
└─ …
Но опять же, воспользуемся значениями по умолчанию:
Теперь текст заголовка слегка смещается, освобождая место для кнопки «Назад».
Анимируйте несколько псевдоэлементов одинаковым образом с помощью view-transition-class
Browser Support
Допустим, у вас есть переход вида с кучей карточек, но также и заголовок на странице. Чтобы анимировать все карточки, кроме заголовка, вам нужно написать селектор, который нацелен на каждую отдельную карточку.
h1 {
view-transition-name: title;
}
::view-transition-group(title) {
animation-timing-function: ease-in-out;
}
#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
…
#card20 { view-transition-name: card20; }
::view-transition-group(card1),
::view-transition-group(card2),
::view-transition-group(card3),
::view-transition-group(card4),
…
::view-transition-group(card20) {
animation-timing-function: var(--bounce);
}
У вас 20 элементов? Это 20 селекторов, которые вам нужно написать. Добавляете новый элемент? Тогда вам также нужно увеличить селектор, который применяет стили анимации. Не совсем масштабируемо.
view-transition-class
можно использовать в псевдоэлементах view transition для применения того же правила стиля.
#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
#card5 { view-transition-name: card5; }
…
#card20 { view-transition-name: card20; }
#cards-wrapper > div {
view-transition-class: card;
}
html::view-transition-group(.card) {
animation-timing-function: var(--bounce);
}
Следующий пример карточек использует предыдущий фрагмент CSS. Все карточки, включая недавно добавленные, получают одинаковое время, применяемое с одним селектором: html::view-transition-group(.card)
.
view-transition-class
он применяет одну и ту же animation-timing-function
ко всем картам, кроме добавленных или удаленных.Отладка переходов
Поскольку переходы между представлениями создаются на основе CSS-анимаций, панель «Анимации» в Chrome DevTools отлично подходит для отладки переходов.
Используя панель «Анимации» , вы можете приостановить следующую анимацию, а затем прокручивать ее вперед и назад. Во время этого псевдоэлементы перехода можно найти на панели «Элементы» .
Переходные элементы не обязательно должны быть одним и тем же элементом DOM.
До сих пор мы использовали view-transition-name
для создания отдельных элементов перехода для заголовка и текста в заголовке. Это концептуально один и тот же элемент до и после изменения DOM, но вы можете создавать переходы там, где это не так.
Например, основному встроенному видео можно присвоить view-transition-name
:
.full-embed {
view-transition-name: full-embed;
}
Затем, когда на миниатюру нажимают, ей можно присвоить то же самое view-transition-name
, только на время перехода:
thumbnail.onclick = async () => {
thumbnail.style.viewTransitionName = 'full-embed';
document.startViewTransition(() => {
thumbnail.style.viewTransitionName = '';
updateTheDOMSomehow();
});
};
И вот результат:
Теперь миниатюра переходит в основное изображение. Несмотря на то, что это концептуально (и буквально) разные элементы, API перехода рассматривает их как одно и то же, поскольку они имеют одно и то же view-transition-name
.
Настоящий код для этого перехода немного сложнее, чем в предыдущем примере, поскольку он также обрабатывает переход обратно на страницу миниатюр. Полную реализацию см. в исходном коде .
Пользовательские входные и выходные переходы
Посмотрите на этот пример:
Боковая панель является частью перехода:
.sidebar {
view-transition-name: sidebar;
}
Но, в отличие от заголовка в предыдущем примере, боковая панель не отображается на всех страницах. Если боковая панель есть в обоих состояниях, псевдоэлементы перехода выглядят так:
::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
└─ ::view-transition-image-pair(sidebar)
├─ ::view-transition-old(sidebar)
└─ ::view-transition-new(sidebar)
Однако, если боковая панель находится только на новой странице, псевдоэлемент ::view-transition-old(sidebar)
не будет там. Поскольку для боковой панели нет 'старого' изображения, пара изображений будет иметь только ::view-transition-new(sidebar)
. Аналогично, если боковая панель находится только на старой странице, пара изображений будет иметь только ::view-transition-old(sidebar)
.
В предыдущей демонстрации боковая панель переходит по-разному в зависимости от того, входит ли она в состояние входа, выхода или присутствует в обоих состояниях. Она входит, скользя справа и постепенно появляясь, она выходит, скользя вправо и постепенно исчезая, и она остается на месте, когда присутствует в обоих состояниях.
Чтобы создать определенные входные и выходные переходы, можно использовать псевдокласс :only-child
для нацеливания на старые или новые псевдоэлементы, когда это единственный дочерний элемент в паре изображений:
/* Entry transition */
::view-transition-new(sidebar):only-child {
animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
/* Exit transition */
::view-transition-old(sidebar):only-child {
animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}
В этом случае не существует определенного перехода для случая, когда боковая панель присутствует в обоих состояниях, поскольку состояние по умолчанию идеально.
Асинхронные обновления DOM и ожидание контента
Обратный вызов, переданный в .startViewTransition()
может возвращать обещание, которое позволяет выполнять асинхронные обновления DOM и ожидать готовности важного контента.
document.startViewTransition(async () => {
await something;
await updateTheDOMSomehow();
await somethingElse;
});
Переход не будет запущен, пока не выполнится обещание. В это время страница заморожена, поэтому задержки здесь должны быть сведены к минимуму. В частности, сетевые выборки должны выполняться до вызова .startViewTransition()
, пока страница все еще полностью интерактивна, а не как часть обратного вызова .startViewTransition()
.
Если вы решили дождаться готовности изображений или шрифтов, обязательно используйте агрессивный тайм-аут:
const wait = ms => new Promise(r => setTimeout(r, ms));
document.startViewTransition(async () => {
updateTheDOMSomehow();
// Pause for up to 100ms for fonts to be ready:
await Promise.race([document.fonts.ready, wait(100)]);
});
Однако в некоторых случаях лучше вообще избежать задержки и использовать тот контент, который у вас уже есть.
Извлеките максимум пользы из уже имеющегося у вас контента
В случае перехода миниатюры в более крупное изображение:
Переход по умолчанию — плавный переход, то есть миниатюра может плавно переходить в еще не загруженное полное изображение.
Один из способов справиться с этим — дождаться полной загрузки изображения перед началом перехода. В идеале это следует делать до вызова .startViewTransition()
, чтобы страница оставалась интерактивной, а спиннер можно было бы показать пользователю, что идет загрузка. Но в этом случае есть способ получше:
::view-transition-old(full-embed),
::view-transition-new(full-embed) {
/* Prevent the default animation,
so both views remain opacity:1 throughout the transition */
animation: none;
/* Use normal blending,
so the new view sits on top and obscures the old view */
mix-blend-mode: normal;
}
Теперь миниатюра не исчезает, она просто находится под полным изображением. Это означает, что если новый вид не загрузился, миниатюра будет видна на протяжении всего перехода. Это означает, что переход может начаться сразу, а полное изображение может загрузиться в свое время.
Это не сработало бы, если бы новое представление имело прозрачность, но в данном случае мы знаем, что это не так, поэтому мы можем выполнить эту оптимизацию.
Обработка изменений в соотношении сторон
Удобно, что все переходы до сих пор были к элементам с одинаковым соотношением сторон, но это не всегда так. Что, если миниатюра 1:1, а основное изображение 16:9?
В переходе по умолчанию группа анимируется от размера до размера после. Старый и новый виды имеют 100% ширины группы и автовысоту, что означает, что они сохраняют соотношение сторон независимо от размера группы.
Это хорошее значение по умолчанию, но это не то, что нужно в данном случае. Итак:
::view-transition-old(full-embed),
::view-transition-new(full-embed) {
/* Prevent the default animation,
so both views remain opacity:1 throughout the transition */
animation: none;
/* Use normal blending,
so the new view sits on top and obscures the old view */
mix-blend-mode: normal;
/* Make the height the same as the group,
meaning the view size might not match its aspect-ratio. */
height: 100%;
/* Clip any overflow of the view */
overflow: clip;
}
/* The old view is the thumbnail */
::view-transition-old(full-embed) {
/* Maintain the aspect ratio of the view,
by shrinking it to fit within the bounds of the element */
object-fit: contain;
}
/* The new view is the full image */
::view-transition-new(full-embed) {
/* Maintain the aspect ratio of the view,
by growing it to cover the bounds of the element */
object-fit: cover;
}
Это означает, что миниатюра остается в центре элемента при увеличении ширины, но полное изображение «расправляется» при переходе от соотношения 1:1 к 16:9.
Более подробную информацию см. в разделе Просмотр переходов: Обработка изменений соотношения сторон.
Используйте медиа-запросы для изменения переходов для различных состояний устройства
Возможно, вы захотите использовать разные переходы на мобильных устройствах и компьютерах, как в этом примере, где на мобильном устройстве выполняется полный слайд сбоку, а на компьютере — более плавный:
Этого можно добиться с помощью обычных медиа-запросов:
/* Transitions for mobile */
::view-transition-old(root) {
animation: 300ms ease-out both full-slide-to-left;
}
::view-transition-new(root) {
animation: 300ms ease-out both full-slide-from-right;
}
@media (min-width: 500px) {
/* Overrides for larger displays.
This is the shared axis transition from earlier in the article. */
::view-transition-old(root) {
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}
::view-transition-new(root) {
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
}
Вы также можете захотеть изменить то, каким элементам вы назначаете view-transition-name
в зависимости от соответствующих медиа-запросов.
Реагируйте на предпочтение «ограниченного движения»
Пользователи могут указать, что они предпочитают меньше движений, через свою операционную систему, и эти предпочтения отображаются в CSS .
Вы можете запретить любые переходы для этих пользователей:
@media (prefers-reduced-motion) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
Однако предпочтение «уменьшенного движения» не означает, что пользователь не хочет никакого движения . Вместо предыдущего фрагмента вы можете выбрать более тонкую анимацию, но такую, которая все еще выражает связь между элементами и поток данных.
Обработка нескольких стилей перехода между представлениями с помощью типов перехода между представлениями
Browser Support
Иногда переход от одного конкретного представления к другому должен иметь специально разработанный переход. Например, при переходе на следующую или предыдущую страницу в последовательности страниц вы можете захотеть сдвинуть содержимое в другом направлении в зависимости от того, переходите ли вы на более высокую или более низкую страницу из последовательности.
Для этого можно использовать типы переходов вида, которые позволяют назначить один или несколько типов для активного перехода вида. Например, при переходе на более высокую страницу в последовательности страниц используйте тип forwards
, а при переходе на более низкую страницу используйте тип backwards
. Эти типы активны только при захвате или выполнении перехода, и каждый тип можно настроить с помощью CSS для использования различных анимаций.
Чтобы использовать типы в переходе представления одного документа, вы передаете types
в метод startViewTransition
. Чтобы разрешить это, document.startViewTransition
также принимает объект: update
— это функция обратного вызова, которая обновляет DOM, а types
— это массив с типами.
const direction = determineBackwardsOrForwards();
const t = document.startViewTransition({
update: updateTheDOMSomehow,
types: ['slide', direction],
});
Чтобы реагировать на эти типы, используйте селектор :active-view-transition-type()
. Передайте в селектор type
на который вы хотите нацелиться. Это позволяет вам хранить стили нескольких переходов представлений отдельно друг от друга, без того, чтобы объявления одного мешали объявлениям другого.
Поскольку типы применяются только при захвате или выполнении перехода, вы можете использовать селектор, чтобы задать или отменить задание view-transition-name
для элемента только для view transition с этим типом.
/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
:root {
view-transition-name: none;
}
article {
view-transition-name: content;
}
.pagination {
view-transition-name: pagination;
}
}
/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
&::view-transition-old(content) {
animation-name: slide-out-to-left;
}
&::view-transition-new(content) {
animation-name: slide-in-from-right;
}
}
/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
&::view-transition-old(content) {
animation-name: slide-out-to-right;
}
&::view-transition-new(content) {
animation-name: slide-in-from-left;
}
}
/* Animation styles for reload type only (using the default root snapshot) */
html:active-view-transition-type(reload) {
&::view-transition-old(root) {
animation-name: fade-out, scale-down;
}
&::view-transition-new(root) {
animation-delay: 0.25s;
animation-name: fade-in, scale-up;
}
}
В следующем примере пагинации содержимое страницы скользит вперед или назад в зависимости от номера страницы, на которую вы переходите. Типы определяются по щелчку, после чего они передаются в document.startViewTransition
.
Чтобы выбрать любой активный переход представления, независимо от типа, можно использовать селектор псевдокласса :active-view-transition
.
html:active-view-transition {
…
}
Обработка нескольких стилей перехода между представлениями с помощью имени класса в корне перехода между представлениями
Иногда переход от одного конкретного типа представления к другому должен иметь специально разработанный переход. Или навигация «назад» должна отличаться от навигации «вперед».
До появления типов переходов способ обработки таких случаев заключался во временном задании имени класса в корне перехода. При вызове document.startViewTransition
этот корень перехода является элементом <html>
, доступным с помощью document.documentElement
в JavaScript:
if (isBackNavigation) {
document.documentElement.classList.add('back-transition');
}
const transition = document.startViewTransition(() =>
updateTheDOMSomehow(data)
);
try {
await transition.finished;
} finally {
document.documentElement.classList.remove('back-transition');
}
Чтобы удалить классы после завершения перехода, в этом примере используется transition.finished
— обещание, которое разрешается после того, как переход достигнет своего конечного состояния. Другие свойства этого объекта рассматриваются в справочнике API .
Теперь вы можете использовать это имя класса в своем CSS для изменения перехода:
/* 'Forward' transitions */
::view-transition-old(root) {
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}
::view-transition-new(root) {
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms
cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
/* Overrides for 'back' transitions */
.back-transition::view-transition-old(root) {
animation-name: fade-out, slide-to-right;
}
.back-transition::view-transition-new(root) {
animation-name: fade-in, slide-from-left;
}
Как и в случае с медиа-запросами, наличие этих классов также может использоваться для изменения того, какие элементы получат view-transition-name
.
Выполняйте переходы, не замораживая другие анимации.
Взгляните на эту демонстрацию положения перехода видео:
Вы увидели что-то не так? Не волнуйтесь, если не заметили. Вот замедленный вариант:
Во время перехода видео как будто замирает, а затем воспроизводимая версия видео постепенно появляется. Это происходит потому, что ::view-transition-old(video)
— это снимок экрана старого вида, тогда как ::view-transition-new(video)
— это живое изображение нового вида.
Вы можете это исправить, но сначала спросите себя, стоит ли это исправлять. Если вы не видели «проблему», когда переход воспроизводился с нормальной скоростью, я бы не стал заморачиваться с его изменением.
Если вы действительно хотите это исправить, то не показывайте ::view-transition-old(video)
; переключитесь сразу на ::view-transition-new(video)
. Вы можете сделать это, переопределив стили и анимацию по умолчанию:
::view-transition-old(video) {
/* Don't show the frozen old view */
display: none;
}
::view-transition-new(video) {
/* Don't fade the new view in */
animation: none;
}
И это всё!
Теперь видео воспроизводится на протяжении всего перехода.
Интеграция с Navigation API (и другими фреймворками)
Переходы между представлениями определены таким образом, что их можно интегрировать с другими фреймворками или библиотеками. Например, если ваше одностраничное приложение (SPA) использует маршрутизатор, вы можете настроить механизм обновления маршрутизатора для обновления содержимого с помощью перехода между представлениями.
В следующем фрагменте кода, взятом из этой демонстрационной версии постраничной навигации, обработчик перехвата API навигации настроен на вызов document.startViewTransition
, когда поддерживаются переходы между представлениями.
navigation.addEventListener("navigate", (e) => {
// Don't intercept if not needed
if (shouldNotIntercept(e)) return;
// Intercept the navigation
e.intercept({
handler: async () => {
// Fetch the new content
const newContent = await fetchNewContent(e.destination.url, {
signal: e.signal,
});
// The UA does not support View Transitions, or the UA
// already provided a Visual Transition by itself (e.g. swipe back).
// In either case, update the DOM directly
if (!document.startViewTransition || e.hasUAVisualTransition) {
setContent(newContent);
return;
}
// Update the content using a View Transition
const t = document.startViewTransition(() => {
setContent(newContent);
});
}
});
});
Некоторые, но не все, браузеры предоставляют собственный переход, когда пользователь выполняет жест смахивания для навигации. В этом случае вам не следует запускать собственный переход представления, поскольку это приведет к плохому или запутанному пользовательскому опыту. Пользователь увидит два перехода — один предоставленный браузером, а другой — вами — работающих последовательно.
Поэтому рекомендуется предотвратить запуск перехода представления, когда браузер предоставил свой собственный визуальный переход. Чтобы добиться этого, проверьте значение свойства hasUAVisualTransition
экземпляра NavigateEvent
. Свойство устанавливается в true
, когда браузер предоставил визуальный переход. Это свойство hasUIVisualTransition
также существует в экземплярах PopStateEvent
.
В предыдущем фрагменте проверка, которая определяет, следует ли запускать переход представления, учитывает это свойство. Если нет поддержки переходов представления того же документа или если браузер уже предоставил свой собственный переход, переход представления пропускается.
if (!document.startViewTransition || e.hasUAVisualTransition) {
setContent(newContent);
return;
}
В следующей записи пользователь проводит пальцем, чтобы вернуться на предыдущую страницу. Захват слева не включает проверку флага hasUAVisualTransition
. Запись справа включает проверку, тем самым пропуская ручной переход представления, поскольку браузер предоставил визуальный переход.
hasUAVisualTransition
Анимация с помощью JavaScript
До сих пор все переходы определялись с помощью CSS, но иногда CSS недостаточно:
Некоторые части этого перехода невозможно реализовать только с помощью CSS:
- Анимация начинается с места щелчка.
- Анимация заканчивается тем, что круг имеет радиус до самого дальнего угла. Хотя, надеюсь, это станет возможным с помощью CSS в будущем .
К счастью, вы можете создавать переходы с помощью API веб-анимации !
let lastClick;
addEventListener('click', event => (lastClick = event));
function spaNavigate(data) {
// Fallback for browsers that don't support this API:
if (!document.startViewTransition) {
updateTheDOMSomehow(data);
return;
}
// Get the click position, or fallback to the middle of the screen
const x = lastClick?.clientX ?? innerWidth / 2;
const y = lastClick?.clientY ?? innerHeight / 2;
// Get the distance to the furthest corner
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y)
);
// With a transition:
const transition = document.startViewTransition(() => {
updateTheDOMSomehow(data);
});
// Wait for the pseudo-elements to be created:
transition.ready.then(() => {
// Animate the root's new view
document.documentElement.animate(
{
clipPath: [
`circle(0 at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
],
},
{
duration: 500,
easing: 'ease-in',
// Specify which pseudo-element to animate
pseudoElement: '::view-transition-new(root)',
}
);
});
}
В этом примере используется transition.ready
, обещание, которое разрешается после успешного создания псевдоэлементов перехода. Другие свойства этого объекта рассматриваются в справочнике API .
Переходы как улучшение
API View Transition предназначен для «оборачивания» изменения DOM и создания для него перехода. Однако переход следует рассматривать как улучшение, то есть ваше приложение не должно переходить в состояние «ошибка», если изменение DOM прошло успешно, но переход не удался. В идеале переход не должен удаваться, но если это произошло, он не должен нарушать остальную часть пользовательского опыта.
Чтобы рассматривать переходы как улучшение, будьте осторожны и не используйте обещания перехода таким образом, чтобы это привело к сбою приложения в случае сбоя перехода.
async function switchView(data) { // Fallback for browsers that don't support this API: if (!document.startViewTransition) { await updateTheDOM(data); return; } const transition = document.startViewTransition(async () => { await updateTheDOM(data); }); await transition.ready; document.documentElement.animate( { clipPath: [`inset(50%)`, `inset(0)`], }, { duration: 500, easing: 'ease-in', pseudoElement: '::view-transition-new(root)', } ); }
Проблема с этим примером в том, что switchView()
отклонит, если переход не может достичь состояния ready
, но это не значит, что представление не удалось переключиться. DOM, возможно, успешно обновился, но были дубликаты view-transition-name
s, поэтому переход был пропущен.
Вместо:
async function switchView(data) { // Fallback for browsers that don't support this API: if (!document.startViewTransition) { await updateTheDOM(data); return; } const transition = document.startViewTransition(async () => { await updateTheDOM(data); }); animateFromMiddle(transition); await transition.updateCallbackDone; } async function animateFromMiddle(transition) { try { await transition.ready; document.documentElement.animate( { clipPath: [`inset(50%)`, `inset(0)`], }, { duration: 500, easing: 'ease-in', pseudoElement: '::view-transition-new(root)', } ); } catch (err) { // You might want to log this error, but it shouldn't break the app } }
В этом примере transition.updateCallbackDone
используется для ожидания обновления DOM и отклонения в случае сбоя. switchView
больше не отклоняет переход в случае сбоя, он разрешается после завершения обновления DOM и отклоняет его в случае сбоя.
Если вы хотите, чтобы switchView
разрешался, когда новое представление «установилось», например, любой анимированный переход был завершен или пропущен до конца, замените transition.updateCallbackDone
на transition.finished
.
Не полифилл, но…
Это не простая функция для полифилла. Однако эта вспомогательная функция значительно упрощает работу в браузерах, которые не поддерживают переходы между представлениями:
function transitionHelper({
skipTransition = false,
types = [],
update,
}) {
const unsupported = (error) => {
const updateCallbackDone = Promise.resolve(update()).then(() => {});
return {
ready: Promise.reject(Error(error)),
updateCallbackDone,
finished: updateCallbackDone,
skipTransition: () => {},
types,
};
}
if (skipTransition || !document.startViewTransition) {
return unsupported('View Transitions are not supported in this browser');
}
try {
const transition = document.startViewTransition({
update,
types,
});
return transition;
} catch (e) {
return unsupported('View Transitions with types are not supported in this browser');
}
}
И это можно использовать так:
function spaNavigate(data) {
const types = isBackNavigation ? ['back-transition'] : [];
const transition = transitionHelper({
update() {
updateTheDOMSomehow(data);
},
types,
});
// …
}
В браузерах, не поддерживающих переходы между представлениями, updateDOM
все равно будет вызываться, но анимированного перехода не будет.
Вы также можете указать некоторые classNames
для добавления в <html>
во время перехода, что упрощает изменение перехода в зависимости от типа навигации .
Вы также можете передать true
в skipTransition
, если не хотите анимацию, даже в браузерах, которые поддерживают переходы между представлениями. Это полезно, если на вашем сайте есть пользовательские настройки для отключения переходов.
Работа с фреймворками
Если вы работаете с библиотекой или фреймворком, который абстрагируется от изменений DOM, то самое сложное — узнать, когда изменение DOM завершено. Вот набор примеров, использующих помощник выше , в различных фреймворках.
- React — ключ здесь —
flushSync
, который применяет набор изменений состояния синхронно. Да, есть большое предупреждение об использовании этого API, но Дэн Абрамов уверяет меня, что это уместно в данном случае. Как обычно с React и асинхронным кодом, при использовании различных обещаний, возвращаемыхstartViewTransition
, позаботьтесь о том, чтобы ваш код выполнялся с правильным состоянием. - Vue.js — здесь ключевым является
nextTick
, который выполняется после обновления DOM. - Svelte — очень похож на Vue, но метод ожидания следующего изменения —
tick
. - Lit — ключевым моментом здесь является обещание
this.updateComplete
внутри компонентов, которое выполняется после обновления DOM. - Angular — здесь ключевое значение имеет
applicationRef.tick
, который сбрасывает ожидающие изменения DOM. Начиная с версии Angular 17 вы можете использоватьwithViewTransitions
, который поставляется с@angular/router
.
API-ссылка
-
const viewTransition = document.startViewTransition(update)
Начать новый
ViewTransition
.update
— это функция, которая вызывается после фиксации текущего состояния документа.Затем, когда обещание, возвращенное
updateCallback
выполняется, переход начинается в следующем кадре. Если обещание, возвращенноеupdateCallback
отклоняется, переход отменяется.-
const viewTransition = document.startViewTransition({ update, types })
Запустить новый
ViewTransition
с указанными типамиupdate
вызывается после фиксации текущего состояния документа.types
устанавливает активные типы для перехода при захвате или выполнении перехода. Изначально он пуст. Подробнее см.viewTransition.types
ниже.
Члены экземпляра ViewTransition
:
-
viewTransition.updateCallbackDone
Обещание, которое выполняется, если выполняется обещание, возвращенное
updateCallback
, или отклоняется, если оно отклоняется.API View Transition оборачивает изменение DOM и создает переход. Однако иногда вам не важен успех или неудача анимации перехода, вы просто хотите знать, произошло ли изменение DOM и когда.
updateCallbackDone
предназначен для этого варианта использования.-
viewTransition.ready
Обещание, которое выполняется, как только псевдоэлементы для перехода созданы и анимация вот-вот начнется.
Он отклоняет, если переход не может начаться. Это может быть связано с неправильной конфигурацией, например, дублированием
view-transition-name
s или еслиupdateCallback
возвращает отклоненное обещание.Это полезно для анимации псевдоэлементов перехода с помощью JavaScript .
-
viewTransition.finished
Обещание выполняется, как только конечное состояние становится полностью видимым и интерактивным для пользователя.
Он отклоняется только в том случае, если
updateCallback
возвращает отклоненное обещание, поскольку это означает, что конечное состояние не было создано.В противном случае, если переход не начинается или пропускается во время перехода, конечное состояние все равно достигается, поэтому выполняется
finished
.-
viewTransition.types
Объект типа
Set
, который содержит типы перехода активного представления. Для управления записями используйте его методы экземпляраclear()
,add()
иdelete()
.Чтобы отреагировать на определенный тип в CSS, используйте селектор псевдокласса
:active-view-transition-type(type)
в корне перехода.Типы автоматически очищаются после завершения перехода между представлениями.
-
viewTransition.skipTransition()
Пропустите анимационную часть перехода.
Это не приведет к пропуску вызова
updateCallback
, поскольку изменение DOM отделено от перехода.
Ссылка на стиль по умолчанию и переход
-
::view-transition
- Корневой псевдоэлемент, который заполняет область просмотра и содержит каждую
::view-transition-group
. -
::view-transition-group
Абсолютно позиционируем.
Переходы
width
иheight
между состояниями «до» и «после».Переходы
transform
квадрат пространства вьюпорта «до» и «после».-
::view-transition-image-pair
Полностью готов заполнить группу.
Имеет
isolation: isolate
, чтобы ограничить влияниеmix-blend-mode
на старые и новые представления.-
::view-transition-new
и::view-transition-old
Абсолютно позиционирован в верхнем левом углу обертки.
Заполняет 100% ширины группы, но имеет автоматическую высоту, поэтому сохраняет пропорции, а не заполняет группу.
Имеет
mix-blend-mode: plus-lighter
для обеспечения настоящего перекрестного затухания.Старый вид переходит от
opacity: 1
кopacity: 0
Новый вид переходит отopacity: 0
кopacity: 1
.
Обратная связь
Обратная связь от разработчиков всегда приветствуется. Чтобы сделать это, отправьте сообщение о проблеме в рабочую группу CSS на GitHub с предложениями и вопросами. Добавьте к проблеме префикс [css-view-transitions]
.
Если вы столкнулись с ошибкой, отправьте отчет об ошибке Chromium .